/*
 * Copyright (c) 2007 jMouse Gestures
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMouse Gestures' nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jmg.gesture;

import java.io.Serializable;
import com.jmg.mouse.MouseMovement;


/**
 * GestureMovement class defines a movement that must be made by the mouse.
 * <p>
 * This class is completely immutable and all property values must be set at
 * construction time.
 * </p>
 *
 * @author  Christopher Martin
 * @version $Id: GestureMovement.java 12 2007-05-24 11:54:07Z GentlemanHal $
 */
public class GestureMovement implements 
    Serializable
{
    /** Serial Version UID */
    private static final long serialVersionUID = 1L;
    
    /** Angle in radians that represents North (Up).              */
    public static final float ANGLE_NORTH      = -0.50f * (float) Math.PI;
    /** Angle in radians that represents North-East (Up-Right).   */
    public static final float ANGLE_NORTH_EAST = -0.25f * (float) Math.PI;
    /** Angle in radians that represents East (Right).            */
    public static final float ANGLE_EAST       =  0.00f;
    /** Angle in radians that represents South-East (Down-Right). */
    public static final float ANGLE_SOUTH_EAST =  0.25f * (float) Math.PI;
    /** Angle in radians that represents South (Down).            */
    public static final float ANGLE_SOUTH      =  0.50f * (float) Math.PI;
    /** Angle in radians that represents South-West (Down-Left).  */
    public static final float ANGLE_SOUTH_WEST =  0.75f * (float) Math.PI;
    /** Angle in radians that represents West (Left).             */
    public static final float ANGLE_WEST       =          (float) Math.PI;
    /** Angle in radians that represents North-West (Up-Left).    */
    public static final float ANGLE_NORTH_WEST = -0.75f * (float) Math.PI;
    
    private final float minLength;
    private final float maxLength;
    private final float minAngle;
    private final float maxAngle;
    
    
    
    
    
    /**
     * Creates a new instance of <code>GestureMovement</code>.
     * <p>
     * The angle variables correspond to the following compass directions:
     * <pre><ul>
     *   <li>North      = {@link java.lang.Math#PI -1/2 pi} (-1.57)
     *   <li>North-East = {@link java.lang.Math#PI -1/4 pi} (-0.79)
     *   <li>East       = 0.0
     *   <li>South-East =  {@link java.lang.Math#PI  1/4 pi} ( 0.79)
     *   <li>South      =  {@link java.lang.Math#PI  1/2 pi} ( 1.57)
     *   <li>South-West =  {@link java.lang.Math#PI  3/4 pi} ( 2.36)
     *   <li>West       =      {@link java.lang.Math#PI pi} ( 3.14)
     *   <li>North-West = {@link java.lang.Math#PI -3/4 pi} (-2.36)
     * </ul></pre>
     * These values are used as they are the default range as returned by the
     * {@link java.lang.Math#atan2} method when assuming a coordinate frame
     * with an origin at the top left (moving down = positive y, moving right
     * = positive x).
     * </p><p>
     * It's recommended that the 
     * {@link com.jmg.gesture.GestureFactory#createGestureMovement(float, float, float, int)} 
     * method is used to create <code>GestureMovements</code> instead of using 
     * this constructor directly.
     * 
     * @param  minLength Movements must be greater than this to be valid, must 
     *         be greater than zero and less than maxLength.
     * @param  maxLength Movements must be less than this to be valid, must be
     *         greater than zero and minLength
     * @param  minAngle Angle of the movement must be greater than this to be
     *         valid, must be greater than or equal to 
     *         {@link java.lang.Math#PI -pi} and less than or
     *         equal to {@link java.lang.Math#PI pi}.
     * @param  maxAngle Angle of the movement must be less than this to be
     *         valid, must be greater than or equal to 
     *         {@link java.lang.Math#PI -pi} and less than or
     *         equal to {@link java.lang.Math#PI pi}.
     * @throws java.lang.IllegalArgumentException If:
     *         <ul>
     *           <li>minLength &lt; 0
     *           <li>maxLength &lt; 0
     *           <li>minLength &gt; maxLength
     *           <li>minAngle &lt; {@link java.lang.Math#PI -pi}
     *           <li>minAnlge &gt; {@link java.lang.Math#PI pi}
     *           <li>maxAngle &lt; {@link java.lang.Math#PI pi}
     *           <li>maxAngle &gt; {@link java.lang.Math#PI pi}
     *         </ul>
     * @see    com.jmg.mouse.MouseMovement
     * @see    java.lang.Math#toRadians
     */
    public GestureMovement(final float minLength, final float maxLength, 
                           final float minAngle,  final float maxAngle) 
        throws IllegalArgumentException
    {
        checkExceptions(minLength, maxLength, minAngle, maxAngle);
        
        
        this.minLength = minLength;
        this.maxLength = maxLength;
        this.minAngle  = minAngle;
        this.maxAngle  = maxAngle;
    }

    /**
     * Checks the passed in constructor values for exceptions.
     *
     * @param  minLength Minimum length.
     * @param  maxLength Maximum length.
     * @param  minAngle Minimum angle.
     * @param  maxAngle Maximum angle.
     * @throws java.lang.IllegalArgumentException If an illegal argument is 
     *         passed.
     */
    private void checkExceptions(float minLength, float maxLength, 
                                 float minAngle,  float maxAngle) 
        throws IllegalArgumentException 
    {
        if (minLength < 0f) {
            throw new IllegalArgumentException(
                    "minLength must be > 0: " + minLength);
        }
        
        if (minLength > maxLength) {
            throw new IllegalArgumentException(
                    "minLength must be <= maxLength: min = " + minLength + 
                    ", max = " + maxLength);
        }
        
        if (maxLength < 0f) {
            throw new IllegalArgumentException(
                    "maxLength must be > 0: " + maxLength);
        }
        
        if (minAngle < (float) -Math.PI) {
            throw new IllegalArgumentException(
                    "minAngle must be >= -PI: " + minAngle);
        }
        
        if (minAngle > (float) Math.PI) {
            throw new IllegalArgumentException(
                    "minAngle must be <= PI: " + minAngle);
        }
        
        if (maxAngle < (float) -Math.PI) {
            throw new IllegalArgumentException(
                    "maxAngle must be >= -PI: " + maxAngle);
        }
        
        if (maxAngle > (float) Math.PI) {
            throw new IllegalArgumentException(
                    "maxAngle must be <= PI: " + maxAngle);
        }
    }
    
    
    
    
    
    /**
     * Returns the minimum length, this will always be greater than or equal to
     * zero.
     * 
     * @return The minimum length.
     */
    public float getMinimumLength() {
        return minLength;
    }
    
    /**
     * Returns the maximum length, this will always be greater than or equal to
     * the {@link #getMinimumLength minimum length}.
     * 
     * @return The maximum length.
     */
    public float getMaximumLength() {
        return maxLength;
    }
    
    /**
     * Returns the minimum angle in radians.
     * 
     * @return The minimum angle.
     * @see    java.lang.Math#toDegrees
     */
    public float getMinimumAngle() {
        return minAngle;
    }
    
    /**
     * Returns the maximum angle in radians.
     * 
     * @return The maximum angle.
     * @see    java.lang.Math#toDegrees
     */
    public float getMaximumAngle() {
        return maxAngle;
    }
    
    /**
     * Checks the given movement to see if it is valid.  A valid movement must
     * fall between the 
     * {@link #getMinimumLength minimum length},
     * {@link #getMaximumLength maximum length},
     * {@link #getMinimumAngle  minimum angle} and 
     * {@link #getMaximumLength maximum angle}.
     * 
     * @param  movement The movement to check.
     * @return <code>true</code> if it is valid, <code>false</code> otherwise.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> 
     *         argument is passed.
     */
    public boolean validMovement(MouseMovement movement) 
        throws IllegalArgumentException
    {
        if (null == movement) {
            throw new IllegalArgumentException("movement was null!");
        }
        
        
        return validLength(movement.getLength()) && 
               validAngle(movement.getAngle());
    }
    
    /**
     * Checks if the given length is valid.  A valid length must fall between the 
     * {@link #getMinimumLength minimum length} and
     * {@link #getMaximumLength maximum length}.
     * 
     * @param  length The length to check.
     * @return <code>true</code> if it is valid, <code>false</code> otherwise.
     */
    public boolean validLength(float length) {
        return length >= minLength && length <= maxLength;
    }
    
    /**
     * Checks if the given angle is valid.  A valid angle must fall between the
     * {@link #getMinimumAngle  minimum angle} and 
     * {@link #getMaximumLength maximum angle}.
     * 
     * @param  angle The angle to check.
     * @return <code>true</code> if it is valid, <code>false</code> otherwise.
     */
    public boolean validAngle(float angle) {
        if (angle > ANGLE_WEST || angle < -ANGLE_WEST) {
            angle %= ANGLE_WEST;
        }
        
        if (minAngle >= 0 && maxAngle <= 0) {
            return (angle >= minAngle || angle < 0) && 
                   (angle <= maxAngle || angle > 0);
        }  else {
            return angle >= minAngle && angle <= maxAngle;
        }
    }

    
    
    
    
    /**
     * Indicates whether some other object is "equal to" this one.
     * 
     * @param  obj The reference object with which to compare.
     * @return <code>true</code> if this object is the same as the obj
     *         argument; <code>false</code> otherwise.
     * @see    #hashCode()
     * @see    java.util.Hashtable
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        
        if (null == obj || obj.getClass() != getClass()) {
            return false;
        }
            
        GestureMovement gm = (GestureMovement) obj;
            
        return gm.minAngle  == minAngle  && gm.maxAngle  == maxAngle &&
               gm.minLength == minLength && gm.maxLength == maxLength;
    }

    /**
     * Returns a string representation of the object.
     * 
     * @return a string representation of the object.
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        
        buf.append(getClass().getName());
        buf.append("[angle=").append(minAngle);
        buf.append(" to ").append(maxAngle);
        buf.append(",length=").append(minLength);
        buf.append(" to ").append(maxLength);
        buf.append(']');
        
        return buf.toString();
    }

    /**
     * Returns a hash code value for the object.
     * 
     * @return a hash code value for this object.
     * @see    java.lang.Object#equals(java.lang.Object)
     * @see    java.util.Hashtable
     */
    @Override
    public int hashCode() {
        int hash = 0;
        
        hash += Float.floatToIntBits(minAngle)  * 13;
        hash += Float.floatToIntBits(maxAngle)  * 13;
        hash += Float.floatToIntBits(minLength) * 13; 
        hash += Float.floatToIntBits(maxLength) * 13;
        
        return hash;
    }
    
}
